﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Red.Controls.ViewModels;
using Red.Core;
using Red.Core.Extensions;
using Red.EngineData.View;

namespace Red.Controls.TimeLine.Core
{
    #region TimeLineId

    /// <summary>
    /// ID  that is used to identify various TimeLine entities like Tracks, Events etc.
    /// </summary>
    public struct TimeLineId : IEquatable< TimeLineId >
    {
        public static TimeLineId InvalidId = new TimeLineId( UInt64.MaxValue );

        #region Data

        private readonly UInt64 _id;

        #endregion

        #region C-tor

        public TimeLineId( UInt64 id )
        {
            _id = id;
        }

        #endregion
        
        #region Properties

        public UInt64 Id
        {
            get { return _id; }
        }

        #endregion

        #region Generic operators

        public static implicit operator TimeLineId( UInt64 id )
        {
            return new TimeLineId( id );
        }

        public static implicit operator TimeLineId( string idString )
        {
            UInt64 result;
            UInt64.TryParse( idString, out result );

            return new TimeLineId( result );
        }

        public override string ToString()
        {
            return _id.ToString();
        }

        #endregion

        #region IEquatable implementation

        public bool Equals( TimeLineId other )
        {
            return _id == other._id;
        }

        public static bool operator == ( TimeLineId lhs, TimeLineId rhs )
        {
            return lhs._id == rhs._id;
        }
        public static bool operator !=( TimeLineId lhs, TimeLineId rhs )
        {
            return lhs._id != rhs._id; ;
        }

        public override bool Equals( Object obj )
        {
            return obj is TimeLineId && this == (TimeLineId)obj;
        }
        public override int GetHashCode()
        {
            return _id.GetHashCode();
        }

        #endregion
    }

    #endregion

    #region TimeLine Backend Observer

    public interface ITimeLineBackendObserver
    {
        Task OnTrackCreatedAsync( TimeLineId parentId, TimeLineId trackId );
        Task OnTrackDeletedAsync( TimeLineId parentId, TimeLineId trackId );

        Task OnEventCreatedAsync( TimeLineId trackId, TimeLineId eventId );
        Task OnEventDeletedAsync( TimeLineId trackId, TimeLineId eventId );

        Task OnSectorCreatedAsync( TimeLineId sectorId );
        void OnSectorDeletedAsync( TimeLineId sectorId );
    }

    #endregion

    #region Time Line Backend Proxy

    /// <summary>
    /// Communication Proxy between c# and c++ viva Interop
    /// 
    /// It mantains all entities hierarchy (tracks, events etc.) and exposes raw entities data. Raw data have to be
    /// wrapped by custom ModelView wrappers before uses it by c#. In TimeLine, using of raw InteropDataItem
    /// should be avoided.
    /// 
    /// TODO: Add TimeLineDummyBackedProxy
    /// </summary>
    public class TimeLineBackendProxy
    {
        #region Data

        private readonly ITimeLineBackendObserver _observer = null;

        private DataView _backendView = null;
        private InteropDataItem _backendDataItem = null;

        private DataItem _tracksDataItem = null;
        private DataItem _eventsDataItem = null;
        private DataItem _sectorsDataItem = null;

        private TimeLineId _tracksRootId;

        #endregion

        #region C-tor

        public TimeLineBackendProxy( ITimeLineBackendObserver observer )
        {
            _observer = observer;
        }

        #endregion
       
        #region Public methods

        public async Task InitializeAsync( DataItem backendView )
        {
            if ( backendView == null )
            {
                throw new ArgumentNullException();
            }

            _backendDataItem = backendView as InteropDataItem;

            // Gets and cache needed subitems
            await _backendDataItem.RebuildAsync();
            _backendView = _backendDataItem.Data;

            _tracksDataItem = await _backendDataItem.DigUpSubItemAsync( "tracks" );
            await _tracksDataItem.RebuildAsync();

            _eventsDataItem = await _backendDataItem.DigUpSubItemAsync( "events" );
            await _eventsDataItem.RebuildAsync();

            _sectorsDataItem = await _backendDataItem.DigUpSubItemAsync( "sectors" );
            await _sectorsDataItem.RebuildAsync();

            // Load all needed settings and values
            _tracksRootId = await _tracksDataItem.GetSubValueAsync<UInt64>( "RootId" );

            // Setup notifiers
            await _backendDataItem.RegisterNotifierAsync( "_onTimeLineChanged", OnTimeLineChanged );
            await _backendDataItem.RegisterNotifierAsync( "_onTracksHierarchyChanged", OnTrackHierarchyChanged );
            await _backendDataItem.RegisterNotifierAsync( "_onEventsHierarchyChanged", OnEventHierarchyChanged );
            await _backendDataItem.RegisterNotifierAsync( "_onSectorsHierarchyChanged", OnSectorsHierarchyChanged );
        }

        #endregion

        #region Tracks Handling

        public TimeLineId GetTracksRootId()
        {
            return _tracksRootId;
        }

        public Task<Result<string>> GetTrackTypeAsync( TimeLineId id )
        {
            return _tracksDataItem.ExecuteAsync<String>( "getTrackType", id );
        }

        public async Task GetTrackSubtracksAsync( TimeLineId trackId, List<TimeLineId> children )
        {
            children.Clear();

            DataItem childrenArray = await _tracksDataItem.DigUpSubItemAsync( "subtracks[\"" + trackId + "\"]" );
            await childrenArray.RebuildAsync();

            foreach ( DataItem item in childrenArray.Children )
            {
                UInt64 result;
                if ( UInt64.TryParse( item.Value, out result ) )
                {
                    children.Add( new TimeLineId( result ) );
                }
            }
        }

        public async Task GetTrackEventsAsync( TimeLineId trackId, List<TimeLineId> children )
        {
            children.Clear();

            DataItem childrenArray = await _tracksDataItem.DigUpSubItemAsync( "attachedEvents[\"" + trackId + "\"]" );
            await childrenArray.RebuildAsync();

            foreach ( DataItem item in childrenArray.Children )
            {
                UInt64 result;
                if ( UInt64.TryParse( item.Value, out result ) )
                {
                    children.Add( new TimeLineId( result ) );
                }
            }
        }

        public void ExecuteAddTrack( TimeLineId parentId, string type )
        {
            _tracksDataItem.ExecuteAsync( "createTrack", true, parentId, type ).DoNotAwait();
        }

        public async Task<TimeLineId> ExecuteAddTrackAsync( TimeLineId parentId, string type )
        {
            // false - means that not notification from interop will be called. We need to call it by hand
            UInt64 rawId = ( await _tracksDataItem.ExecuteAsync<UInt64>( "createTrack", false, parentId, type ) ).Value;

            TimeLineId trackId = new TimeLineId( rawId );

            if ( trackId != TimeLineId.InvalidId )
            {
                await _observer.OnTrackCreatedAsync( parentId, trackId );
                return trackId;
            }           

            return TimeLineId.InvalidId;
        }

        public void ExecuteRemoveTrack( TimeLineId trackId )
        {
            _tracksDataItem.ExecuteAsync( "removeTrack", trackId ).DoNotAwait();
        }

        public async Task<DataItemViewModel> GetTrackRawDataAsync( TimeLineId trackId )
        {
            DataItem dataItem = await _tracksDataItem.DigUpSubItemAsync( "data[\"" + trackId + "\"]" );

            DataItemViewModel model = new DataItemViewModel();
            await model.InitializeAsync( dataItem );

            return model; 
        }

        /// <summary>
        /// Callback fired when backend add or remove a Track object
        /// </summary>
        private void OnTrackHierarchyChanged( DataItem item, string notification )
        {
            Command command = ParseCommand( notification );

            if ( command.name == "TrackAdded" )
            {
                _observer.OnTrackCreatedAsync( command.firstId, command.secondId );
                return;
            }

            if ( command.name == "TrackDeleted" )
            {
                _observer.OnTrackDeletedAsync( command.firstId, command.secondId );
                return;
            }            
        }

        #endregion

        #region Events Handling

        public void ExecuteRemoveEvent( TimeLineId eventId )
        {
            _eventsDataItem.ExecuteAsync( "removeEvent", eventId ).DoNotAwait();
        }

        public void ExecuteAddEvent( TimeLineId trackId, string type )
        {
            _eventsDataItem.ExecuteAsync( "createEvent", true, trackId, type ).DoNotAwait();
        }
        
        public async Task<TimeLineId> ExecuteAddEventAsync( TimeLineId trackId, string type )
        {
            // false - means that not notification from interop will be called. We need to call it by hand
            UInt64 rawId = ( await _eventsDataItem.ExecuteAsync<UInt64>( "createEvent", false, trackId, type ) ).Value;

            TimeLineId eventId = new TimeLineId( rawId );
            if ( eventId != TimeLineId.InvalidId )
            {
                await _observer.OnEventCreatedAsync( trackId, eventId );
                return eventId;
            }

            return TimeLineId.InvalidId;            
        }

        public Task<Result<string>> GetEventTypeAsync( TimeLineId id )
        {
            return _eventsDataItem.ExecuteAsync<String>( "getEventType", id );
        }

        public async Task<DataItemViewModel> GetEventRawDataAsync( TimeLineId trackId )
        {
            DataItem dataItem = await _eventsDataItem.DigUpSubItemAsync( "data[\"" + trackId + "\"]" );

            DataItemViewModel model = new DataItemViewModel();
            await model.InitializeAsync( dataItem );

            return model;
        }

        public void ExecuteSelectedItem( TimeLineId id )
        {
            _eventsDataItem.ExecuteAsync( "selectEvent", id ).DoNotAwait();
        }

        private async void OnEventHierarchyChanged( DataItem item, string notification )
        {
            Command command = ParseCommand( notification );

            if ( command.name == "EventAdded" )
            {
                await _observer.OnEventCreatedAsync( command.firstId, command.secondId );
                return;
            }

            if ( command.name == "EventDeleted" )
            {
                await _observer.OnEventDeletedAsync( command.firstId, command.secondId );
                return;
            }
        }

        #endregion

        #region Sectors handling

        public Task<Result<string>> GetSectorTypeAsync( TimeLineId id )
        {
            return _sectorsDataItem.ExecuteAsync<String>( "getSectorType", id );
        }

        public async Task GetSectorsAsync( List<TimeLineId> children )
        {
            children.Clear();

            DataItem childrenArray = await _sectorsDataItem.DigUpSubItemAsync( "sectors" );
            await childrenArray.RebuildAsync();

            foreach ( DataItem item in childrenArray.Children )
            {
                UInt64 result;
                if ( UInt64.TryParse( item.Value, out result ) )
                {
                    children.Add( new TimeLineId( result ) );
                }
            }
        }

        public async Task GetSectorsEventsAsync( TimeLineId trackId, List<TimeLineId> children )
        {
            children.Clear();

            DataItem childrenArray = await _sectorsDataItem.DigUpSubItemAsync( "attachedEvents[\"" + trackId + "\"]" );
            await childrenArray.RebuildAsync();

            foreach ( DataItem item in childrenArray.Children )
            {
                UInt64 result;
                if ( UInt64.TryParse( item.Value, out result ) )
                {
                    children.Add( new TimeLineId( result ) );
                }
            }
        }

        public void ExecuteAddSector( TimeLineId parentId, string type )
        {
            _sectorsDataItem.ExecuteAsync( "createSector", true, parentId, type ).DoNotAwait();
        }

        public async Task<TimeLineId> ExecuteAddSectorAsync( string type )
        {
            // false - means that not notification from interop will be called. We need to call it by hand
            UInt64 rawId = ( await _sectorsDataItem.ExecuteAsync<UInt64>( "createSector", false, type ) ).Value;

            TimeLineId sectorId = new TimeLineId( rawId );

            if ( sectorId != TimeLineId.InvalidId )
            {
                await _observer.OnSectorCreatedAsync( sectorId );
                return sectorId;
            }

            return TimeLineId.InvalidId;
        }

        public void ExecuteRemoveSector( TimeLineId trackId )
        {
            _sectorsDataItem.ExecuteAsync( "removeSector", trackId ).DoNotAwait();
        }

        public async Task<DataItemViewModel> GetSectorRawDataAsync( TimeLineId trackId )
        {
            DataItem dataItem = await _sectorsDataItem.DigUpSubItemAsync( "data[\"" + trackId + "\"]" );

            DataItemViewModel model = new DataItemViewModel();
            await model.InitializeAsync( dataItem );

            return model;
        }

        /// <summary>
        /// Callback fired when backend add or remove a Sector object
        /// </summary>
        private void OnSectorsHierarchyChanged( DataItem item, string notification )
        {
            Command command = ParseCommand( notification );

            if ( command.name == "SectorAdded" )
            {
                _observer.OnSectorCreatedAsync( command.firstId );
                return;
            }

            if ( command.name == "SectorDeleted" )
            {
                _observer.OnSectorDeletedAsync( command.firstId );
                return;
            }
        }

        #endregion

        #region Private methods

        private void OnTimeLineChanged( DataItem item, string notification )
        {
            throw new NotImplementedException();
        }
       
        protected void HandleException( Exception ex )
        {
            throw new Exception( "TimeLineBackendProxy rethrowing: ", ex );
        }
        
        private struct Command
        {
            public string name;
            public TimeLineId firstId;
            public TimeLineId secondId;
        }

        Command ParseCommand( string notificationString )
        {
            // Notification event data format "NotificationName ParentId NewCreatedTrackId"
            string[] parameters = notificationString.Split( ' ' );
            if ( parameters.Length != 3 )
            {
                throw new ArgumentException();
            }

            Command command = new Command();

            command.name = parameters[0];
            command.firstId = parameters[1];
            command.secondId = parameters[2];

            return command;
        }

        #endregion
    }

    #endregion
}
